from typing import (
    Any, Dict, Generic, List, NewType,
    Tuple, Type, TypeVar, Union
)

from pydantic import BaseModel
from tortoise.expressions import Q
from tortoise.models import Model

Total = NewType("Total", int)
ModelType = TypeVar("ModelType", bound=Model)
CreateSchemaType = TypeVar("CreateSchemaType", bound=BaseModel)
UpdateSchemaType = TypeVar("UpdateSchemaType", bound=BaseModel)

class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]):
    
    @classmethod
    def get_model(cls) -> ModelType:
        raise NotImplementedError("Method not implemented, database model unavailable")

    @classmethod
    async def get(cls, id: int) -> ModelType:
        return await cls.get_model().get(id=id)

    @classmethod
    async def list(cls, page: int=0, page_size: int=0, search: Q = Q(), order: list = []) -> Tuple[Total, List[ModelType]]:
        query = cls.get_model().filter(search)
        if page <= 0 or page_size < 0:
            return await query.count(), await query.order_by(*order)
        else:
            return await query.count(), await query.offset((page - 1) * page_size).limit(page_size).order_by(*order)

    @classmethod
    async def create(cls, obj_in: Union[CreateSchemaType, ModelType, Dict[str, Any]]) -> ModelType:
        obj = None
        model = cls.get_model()
        if isinstance(obj_in, model):
            obj = obj_in
        elif isinstance(obj_in, Dict):
            obj = model(**obj_in)
        else:
            obj = model(**obj_in.model_dump())
        await obj.save()
        return obj

    @classmethod
    async def update(cls, id: int, obj_in: Union[UpdateSchemaType, Dict[str, Any]]) -> ModelType:
        if isinstance(obj_in, Dict):
            obj_dict = obj_in
        else:
            obj_dict = obj_in.model_dump(exclude_unset=True)
        obj = await cls.get(id=id)
        obj = obj.update_from_dict(obj_dict)
        await obj.save()
        return obj

    @classmethod
    async def remove(cls, id: int) -> None:
        obj = await cls.get(id=id)
        await obj.delete()